from math import ceil

import datetime

import pytz
from bson import CodecOptions, SON
from bson import ObjectId
from jsonschema import Draft4Validator


class Document(SON):
    _name = None
    _schema = {

    }

    @classmethod
    def db(cls, name=None):
        from tank import mongo
        opts = CodecOptions(document_class=cls)
        return mongo.db[name or cls._name].with_options(codec_options=opts)

    @classmethod
    def with_db(cls, name=None):
        class WithDBDocument(cls):
            _name = name

        return WithDBDocument

    @classmethod
    def aggregate(cls, *args, **kwargs):
        return cls.db().aggregate(*args, **kwargs)

    @classmethod
    def insert_one(cls, *args, **kwargs):
        return cls.db().insert_one(*args, **kwargs)

    @classmethod
    def delete_one(cls, oid):
        return cls.db().delete_one({'_id': ObjectId(oid)})

    @classmethod
    def update_many(cls, *args, **kwargs):
        return cls.db().update_many(*args, **kwargs)

    @classmethod
    def find(cls, *args, **kwargs):
        return cls.db().find(*args, **kwargs)

    @classmethod
    def find_page(cls, page=1, per_page=20, *args, **kwargs):
        cursor = cls.find(*args, **kwargs)
        skip = (page - 1) * per_page
        return cursor.skip(skip).limit(per_page), cursor.count()

    @classmethod
    def find_one(cls, *args, **kwargs):
        return cls.db().find_one(*args, **kwargs)

    @classmethod
    def find_one_or_create(cls, **kwargs):
        doc = cls.find_one(kwargs)
        if not doc:
            kwargs.update({'createAt': datetime.datetime.now()})
            cls.insert_one(kwargs)
            doc = cls.find_one(kwargs)
        return doc

    @classmethod
    def find_one_and_update(cls, *args, **kwargs):
        return cls.db().find_one_and_update(*args, **kwargs)

    @classmethod
    def find_by_oid(cls, oid):
        doc = cls.find_one({'_id': ObjectId(str(oid))})
        if not doc:
            return None
        return doc

    @property
    def oid(self):
        return self['_id'] if self.has_key('_id') else None

    def validate(self):
        Draft4Validator(self._schema, types={"ObjectId": (ObjectId,)}).validate(self)

    def save(self, other=None):
        if isinstance(other, dict):
            self.update(other)
        now = pytz.timezone("Asia/Shanghai").localize(datetime.datetime.now())
        self['dateUpdated'] = now

        _id = None
        if '_id' in self:
            _id = self.pop('_id')

        self.validate()
        if _id:
            self.find_one_and_update({'_id': _id}, {'$set': self})
        else:
            self['_delete'] = False
            self['dateCreated'] = now
            result = self.insert_one(self)
            _id = result.inserted_id
        self['_id'] = _id

    def remove(self):
        self.delete_one(self.oid)


class DocumentPagination(object):
    def __init__(self, cursor, page=1, per_page=20):
        self.cursor = cursor
        self.page = page
        self.per_page = per_page
        self.total_count = cursor.count()
        self.cursor = self.cursor.skip((max(0, page - 1)) * per_page).limit(per_page)

    def __iter__(self):
        return iter(self.cursor)

    @property
    def pages(self):
        return int(ceil(self.total_count / float(self.per_page)))

    @property
    def has_prev(self):
        return self.page > 1

    @property
    def has_next(self):
        return self.page < self.pages

    def iter_pages(self, left_edge=2, left_current=2,
                   right_current=5, right_edge=2):
        last = 0
        for num in range(1, self.pages + 1):
            if num <= left_edge \
                    or (num > self.page - left_current - 1 and num < self.page + right_current) \
                    or num > self.pages - right_edge:
                if last + 1 != num:
                    yield None
                yield num
                last = num

    def to_dict(self):
        return {
            'items': list(self.cursor),
            'page': self.page,
            'pages': self.pages,
            'size': self.per_page,
            'total': self.total_count,
        }


def json_convert(obj):
    """Recursive helper method that converts BSON types so they can be
    converted into json.
    """
    from bson import SON
    from bson.py3compat import iteritems, text_type
    if hasattr(obj, 'iteritems') or hasattr(obj, 'items'):  # PY3 support
        return SON(((k, json_convert(v)) for k, v in iteritems(obj)))
    elif hasattr(obj, '__iter__') and not isinstance(obj, (text_type, bytes)):
        return list((json_convert(v) for v in obj))
    try:
        return _convert_default(obj)
    except TypeError:
        return obj


def _convert_default(obj):
    """对bson.json_util中的default转换进行调整:
       - 使用hashid替换objectId(oid)
         https://github.com/davidaurelio/hashids-python
         http://hashids.org/
       - 不包括数据类型, 如$date, $oid等?
    """
    # TODO: [IMP] datetime?
    from bson import json_util
    from bson.objectid import ObjectId
    if isinstance(obj, ObjectId):
        # from . import hashids
        # return hashids.encode_hex(str(obj))
        return str(obj)
    if isinstance(obj, datetime.datetime):
        result = json_util.default(obj)
        return result['$date']
    return json_util.default(obj)
